МИНОБРНАУКИ  РОССИИ

Федеральное государственное бюджетное образовательное

учреждение  высшего образования

«ДАГЕСТАНСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ»

Факультет управления

 

 

 

КУРС ЛЕКЦИИ ПО ДИСЦИПЛИНЕ

Функциональное программирование и интеллектуальные системы

 

Кафедра математическое моделирование, эконометрика и статистика факультета  управления

Образовательная программа

38.03.05 Бизнес-информатика

 

 

 

 

Разработчик: кафедра «Математическое моделирование, эконометрика и статистика», Рабаданова Р.М. к.э.н., доцент

 

 

Махачкала, 2020 год

 

 

 


 

Тема 1. Введение в функциональное программирование.

 

1. Введение.

2. Функциональный стиль программирования

 

Функциональное программирование – это ветвь программирования, при котором программирование ведется с помощью определения функций. Функциональном программировании нет ни процедур, ни циклов, нет даже переменных. Почти одни только функции. Функциональное программирование обладает рядом очень существенных преимуществ, которые не только позволяют ему существовать наряду с традиционным программированием, но и иметь своих поклонников, свою нишу задач и хорошие перспективы на будущее.

Традиционное программирование родилось в 40-х годах 20 века, когда велась разработка первых электронно-вычислительных машин (ЭВМ). Его основой послужила концепция фон Неймана о хранимой программе автоматических вычислений по заданному алгоритму. Существенными чертами такой программы служили, во-первых, строгая последовательность в выполнении ее элементарных шагов и, во-вторых, возможность хранения и изменения программы наряду с данными для вычислений в общей с ними памяти. Таким образом, программа сама становилась объектом обработки вместе с арифметическими значениями, над которыми, собственно, и должны были производиться все действия.

Когда появились первые компьютеры, то их устройство следовало принципам, сформулированным фон Нейманом (архитектура фон Неймана). Исполнение программ в первых электронно-вычислительных машинах сводилось к выполнению арифметическим устройством (позже оно стало называться процессором) элементарных шагов, называемых командами, которые строго последовательно производили определенные действия над арифметическими значениями или другими командами, хранящимися в оперативной памяти компьютера. Изменять команды нужно было для того, чтобы организовать циклическое повторение участков программы и своевременное завершение таких повторений.

В конце 50-х годов 20 века появились первые языки программирования высокого уровня, в них уже произошел существенный отход от принципов фон Неймана. Во-первых, программа раз и навсегда была отделена от данных. Во-вторых, во время исполнения программы ее текст оставался неизменным, а организация циклического повторения команд в ходе исполнения программы была возложена на систему программирования, которая уже и должна была перевести (транслировать) текст программы в систему команд компьютера так, чтобы ее исполнение происходило в соответствии с написанным текстом. Единственный принцип, остававшийся неизменным, был принцип последовательного исполнения, в соответствии с которым исполнение программы можно было разложить на строго последовательные элементарные шаги алгоритма. Поэтому до сих пор программирование в традиционном стиле часто называют «фон-Неймановским».

Со временем принцип последовательного исполнения стал серьезным препятствием для развития компьютерной техники. Самым узким местом вычислительных систем уже долгое время остается тот самый процессор, который последовательно исполняет элементарные команды. Конечно, скорость работы современных процессоров не сравнить со скоростью работы арифметических устройств первых ЭВМ, однако, производительность компьютеров сейчас, как и раньше, ограничена, в основном, именно этим центральным устройством. Именно скорость работы центрального процессора имеет решающее значение при определении общей производительности компьютера.

Скорость работы процессора стала зависеть уже не столько от его архитектуры и технологических элементов, сколько просто от его размеров, потому что на скорость работы решающее влияние стала оказывать скорость прохождения сигналов по цепям процессора, которая, как известно, не может превысить скорости света. Чем меньше процессор, тем быстрее смогут внутри него проходить сигналы, и тем больше оказывается конечная производительность процессора. Размеры процессора уменьшились многократно, однако, все труднее стало отводить от такого миниатюрного устройства вырабатываемое при работе его элементов тепло. Перед производителями вычислительной аппаратуры встал очень серьезный вопрос: дальнейшее повышение производительности стало почти невозможным без изменения основополагающего принципа всего современного программирования – последовательного исполнения команд.

Конечно, можно так спроектировать вычислительную систему, чтобы в ней могли одновременно работать несколько процессоров, но, к сожалению, это почти не дает увеличения производительности, потому что все программы, написанные на традиционных языках программирования, предполагают последовательное выполнение элементарных шагов алгоритма почти так же, как это было во времена фон Неймана. Время от времени предпринимаются попытки ввести в современные языки программирования конструкции для параллельного выполнения фрагментов кода, однако языки "сопротивляются". Оказывается, думать об организации параллельного выполнения фрагментов программы – этодовольно сложная задача, которая успешно решается человеком только для весьма ограниченных случаев.

Проблему можно решать различными способами. Во-первых, можно попробовать написать специальную программу, которая могла бы проанализировать имеющийся программный текст и автоматически выделить в ней фрагменты, которые можно выполнять параллельно. К сожалению, такой анализ произвольного программного кода очень труден. Последовательность выполнения шагов алгоритма очень трудно предсказать по внешнему виду программы, даже если программа «хорошо структурирована». Второй способ перейти к параллельным вычислениям – это создать такой язык программирования, в котором сам алгоритм имел бы не последовательную структуру, а допускал бы независимое исполнение отдельных частей алгоритма. Но против этого восстает весь накопленный программистами опыт написания программ.

Тем не менее, оказалось, что опыт написания программ, не имеющих строго последовательной структуры, на самом деле есть. Почти одновременно с первым "традиционным" языком программирования – Фортраном появился еще один совершенно непохожий на него язык программирования – Лисп, для которого последовательность выполнения отдельных частей написанной программы была несущественной. Ветвь программирования, начатая созданием Лиспа, понемногу развивалась с начала 60-х годов 20 века и привела к появлению целой плеяды очень своеобразных языков программирования, которые удовлетворяли всем требованиям, необходимым для исполнения программ несколькими параллельными процессорами. Во-первых, алгоритмы, записанные с помощью этих языков, допускают сравнительно простой анализ и формальные преобразования программ, а во-вторых, отдельные части программ могут исполняться независимо друг от друга. Языки, обладающие такими замечательными свойствами – это и есть языки функционального программирования.

Помимо своей хорошей приспособленности к параллельным вычислениям языки функционального программирования обладают еще рядом приятных особенностей. Программы на этих языках записываются коротко, часто много короче, чем в любом другом традиционном (императивном) языке. Описание алгоритмов в функциональном стиле сосредоточено не на том, как достичь нужного результата (в какой последовательности выполнять шаги алгоритма), а больше на том, что должен представлять собой этот результат.

Пожалуй, единственный серьезный недостаток функционального стиля программирования состоит в том, что этот стиль не универсальный. Многие действительно последовательные процессы, такие как поведение программных моделей в реальном времени, игровые и другие программы, организующие взаимодействие компьютера с человеком, не выразимы в функциональном стиле. Тем не менее, функциональное программирование заслуживает изучения хотя бы еще и потому, что позволяет несколько по-иному взглянуть вообще на процесс программирования, а некоторые приемы программирования, которые, вообще говоря, предназначены для написания программ в чисто функциональном стиле, могут с успехом использоваться и в традиционном программировании.

 

2. Функциональный стиль программирования

 

Часто процесс исполнения программы можно представить схемой, показанной на рисунке 1.1.

Рис. 1.1. Схема простой программы

Конечно, не любую программу можно представить в виде такой схемы, а только такую, которая, получив некоторые входные данные в начале своей работы, затем обрабатывает их, не общаясь с внешней средой, и в конце работы выдает некоторый результат. Часто такую схему работы программы называют «черным ящиком», подразумевая, что в этой схеме нас не интересует, как и в какой последовательности происходят те или иные действия в программе, а интересует только то, какой результат получается в зависимости от входных данных. Можно сказать, что при такой схеме работы результат находится в функциональной зависимости от исходных данных.

Можно привести много примеров простых и сложных программ, имеющих смысл и работающих по этой схеме. Например, программа аналитических преобразований выражений – входными и выходными данными здесь будут выражения, представленные в разной форме. Еще пример: компилятор с некоторого языка программирования – входными данными программы будет текст, написанный на этом языке программирования, а выходными данными – объектный код программы. Третий пример: программа составления расписания учебных занятий – исходные данные для такой программы – набор «пожеланий» к расписанию, а выходные данные – таблица, представляющая расписание занятий. В то же время многие программы выполняются не по схеме «черного ящика». Например, любая программа, организующая активное взаимодействие (диалог) с пользователем, не является преобразователем набора входных данных в выходные, если только не считать, что входными и выходными данными в этом случае является «среда», включающая в том числе и пользователя.

Всякая программа, написанная в функциональном стиле – это программа, представляющая собой функцию, аргументом которой служат входные данные из некоторого допустимого набора, и выдающую определенный результат. Обычно при этом подразумевается, что функция, определяющая поведение программы, на одних и тех же входных данных всегда выдает один и тот же результат, то есть функция детерминированная. Можно заметить, что любая программа, содержащая запросы к пользователю (взаимодействующая с пользователем) – не детерминированная, поскольку ее поведение может зависеть от «поведения пользователя», то есть конкретных данных, введенных пользователем. Отличительными особенностями функций, определяемых любым языком функционального программирования, являются следующие черты:

Каждая функция в программе выдает один и тот же результат на одном и том же  наборе  входных  данных  (аргументов  функции),  то  есть результат работы функции является «повторяемым».

Вычисление функции не может повлиять на результат работы других функций, то есть функции являются «чистыми».

Если программа состоит только из чистых функций, то порядок вычисления аргументов этих функций будет несущественным. Вообще, любые выражения, записанные в такой программе по отдельности, независимо друг от друга (вычисление значений отдельных элементов списка, полей кортежа и т.п.) могут вычисляться в любой последовательности или даже параллельно. При наличии нескольких независимых процессоров, работающих в общей памяти, вычисления, происходящие по программе, составленной только из вызовов чистых функций, легко распараллелить. Уже это свойство функционального стиля программирования привлекает к нему значительный интерес.

Можно ли писать программы в функциональном стиле на традиционном языке программирования? Конечно, да. Если программа представляет из себя набор чистых детерминированных функций, то она будет "функциональной" независимо от того, написана ли она на специальном языке функционального программирования Haskell или на традиционной Java. Рассмотрим, например, задачу вычисления числа вещественных корней заданного квадратного уравнения. Функция, решающая эту задачу должна, получив в качестве исходных данных коэффициенты квадратного уравнения, вычислить его дискриминант, а затем   сформировать   нужный   результат   в   зависимости   от   знака вычисленного дискриминанта. На языке Java функция может выглядеть следующим образом (листинг 1.1).

Листинг 1.1. Функция вычисления числа корней квадратного уравнения

int roots(double a, double b, double c) {

double discr = b * b - 4 * a * c;

if (discr < 0) return 0; else

if (discr == 0) return 1; else

return 2; }

Эта функция, конечно же, детерминированная и «чистая», однако все же с точки зрения функционального стиля она имеет одну «неправильность». Дело в том, что в функции определяется и используется локальная переменная discr, которая используется для запоминания значения дискриминанта квадратного уравнения. В данном случае это никак не влияет на «чистоту» написанной функции, но если бы внутри функции были бы определены другие функции, то результат их работы мог бы быть различным в зависимости от того, когда они вызываются – до присваивания переменной discr нового значения или после него. Поэтому чисто функциональный стиль программирования предполагает полное отсутствие присваиваний в программах.

Кроме того, в чисто функциональных программах нет понятия последовательного вычисления. Каждая функция должна представлять собой суперпозицию обращений к другим функциям. В нашем же примере порядок вычислений задается строго, в нем предписывается, что сначала требуется вычислить значение дискриминанта и присвоить вычисленное значение переменной discr, потом проверить, верно ли, что полученная переменная имеет значение, меньшее нуля, и т.д.

Впрочем, в нашем случае исправить программу, приведя ее в соответствие с принципами функционального стиля программирования, довольно просто. Для этого определим две вспомогательные функции для вычисления дискриминанта квадратного уравнения и для вычисления знака вещественного числа (аналогичную стандартной Math.signum, но выдающую целый результат). В результате получится следующая программа (листинг 1.2).

Листинг 1.2. Функция вычисления корней квадратного уравнения

double discr (double a, double b, double c) {

return b * b - 4 * a * c; } int sign (double x) {

return x < 0 ? -1 : x == 0 ? 0 : 1; } int roots(double a, double b, double c) {

return sign(discr(a, b, c)) + 1;

}

Эта программа уже действительно чисто функциональная. Обратите внимание также и на то, что вместо условных операторов мы в этой программе используем условные выражения, которые соединяют условиями не отдельные части последовательно выполняющейся программы, а отдельные подвыражения. Это тоже является характерной особенностью функционального стиля программирования.

Однако, убрав возможность присваивания значений переменным, мы тем самым сделали бессмысленным использование и одного из самых мощных средств традиционного программирования – циклов. Действительно, циклы имеют смысл только в том случае, если при каждом исполнении тела цикла внешняя среда (совокупность значений доступных в данный момент переменных) хотя бы немного, но меняется. В противном случае цикл либо не исполняется ни разу, либо будет продолжать выполняться бесконечно. Как же в отсутствие циклов написать хотя бы такую простую функцию, как вычисление факториала заданного натурального числа?

Альтернативой циклам являются рекурсивные функции, так что задачу написания функции для вычисления факториала числа все же можно решить на Java в чисто функциональном стиле (листинг 1.3):

Листинг 1.3. Функция вычисления факториала натурального числа

int factorial (int n) {

return n == 0 ? 1 : n * factorial(n-1); }

Тогда, может быть, для того, чтобы писать в функциональном стиле, нужно просто взять привычный язык, ну, скажем, ту же Java, и «выкинуть» из него переменные, присваивания и циклы, вообще любые «последовательные» операторы? Конечно, в результате действительно получится язык, на котором программы будут всегда написаны «в функциональном стиле», но получившийся язык будет слишком бедным для того, чтобы на нем можно было писать действительно полезные программы. В «настоящих» языках функционального программирования с функциями можно работать значительно более гибко, чем позволяется в традиционных языках программирования. Рассмотрим следующую простую задачу.

Пусть требуется определить функцию, с помощью которой можно создавать суперпозицию двух вещественных функций одного вещественного аргумента. Другими словами, требуется описать инструмент, с помощью которого из двух произвольных функций f(x) и g(x) можно было бы получить новую функцию fg(x), результат работы которой определялся бы уравнением fg(x) = f(g(x)). Конечно, решение этой задачи было бы очень полезным в ситуации, когда практически единственным инструментом программирования является определение и вызов различных функций. На первый взгляд кажется, что задачу легко запрограммировать на «чуть-чуть расширенной» Java, в которой достаточно лишь разрешить описывать тип функциональных значений и возвращать функции в качестве результата работы других функций (возможно, эта возможность появится в JDK 7, однако, на момент написания книги эта информация еще не подтверждена окончательно). Еще одна «мелочь», которую мы будем использовать в расширенной Java – это возможность определения нового идентификатора типа с помощью typedef. Поставленная задача может быть решена на расширенной Java следующим образом (листинг 1.4).

Листинг 1.4. Реализация суперпозиции на Java typedef Func = #double(double);

Func comp(Func f, Func g) {

final Func result = #double(double x) { return f(g(x)); };

return result; }

К сожалению, не очень понятно, сможет ли такая программа работать даже в расширенном варианте языка Java. Причина этого заключается в том, что функция result, определенная внутри функции comp, использует глобальные для нее значения функциональных аргументов f и g. После выхода из функции comp связь этих аргументов с фактическими значениями может быть потеряна. Попробуем, скажем, выполнить вызов следующей функции, полученной с помощью comp

comp(sin, cos)(3.14)

При вызове comp(sin, cos) будет образовано новое функциональное значение, представленное функцией result в определении функции comp. Однако функция result ссылается на аргументы f и g, а их связь с функциями sin и cos может быть потеряна после выхода из функции comp. В этом случае вызов новой функции с аргументом 3.14 не сможет проработать правильно. Для того, чтобы эта программа работала правильно, необходимо кардинально пересмотреть подход к образованию функциональных значений.

Описанное поведение заложено в структуре традиционных языков, рассчитанных на последовательное исполнение шагов алгоритма, очень глубоко, и для того, чтобы можно было писать программы для решения задач вроде только что описанной, требуется определять языки с совершенно другими свойствами. Этой цели и служат специализированные языки функционального программирования. В частности, в этих языках функции являются значениями «первого класса», то есть с ними можно проделывать все то же, что и с другими «обычными» значениями: над ними можно выполнять операции и применять к ним другие функции как к

аргументам; можно написать функцию, результатом работы которой будет также функция; функции могут быть элементами сложных структур данных и т.п.

Кстати, программу, содержащую функцию, похожую на comp, все же можно написать и на чистом языке Java. Для этого надо использовать вместо представлений в виде функции представления в виде реализаций некоторого специального интерфейса.


 

Тема 2. Основы функционального программирования на яыке F#

Наверняка в последнее время вы хоть что-то слышали о F# — новейшем пополнении в семействе языков для Microsoft Visual Studio. Для изучения F# есть много веских причинеткий синтаксис, мощные средства работы с множеством потоков и способность к взаимодействию с другими языками, совместимыми с Microsoft .NET Framework. Однако F# включает несколько новых важных концепций, в которые нужно вникнуть, прежде чем пользоваться ими.

Краткий экскурс — неплохой способ приступить к освоению другого объектно-ориентированного языка или даже динамического языка вроде Ruby или Python. Такое возможно потому, что вам уже известна большая часть словарного запаса подобных языков и вы просто изучаете новый синтаксис. Но F# стоит здесь особняком. F# — это язык функционального программирования, и его словарь отличается куда больше, чем вы могли бы ожидать. Более того, функциональные языки традиционно применялись в академических кругах, поэтому определения совершенно новых для вас терминов могут оказаться сложными в понимании.

К счастью, F# проектировали вовсе не как академический язык. Его синтаксис позволяет с помощью методик функционального программирования решать задачи новыми и более эффективными способами, в то же время поддерживая объектно-ориентированные и императивные стили, к которым вы привыкли как .NET-разработчик. В отличие от других .NET-языков структура F# на основе множества парадигм означает, что вы свободны в выборе наиболее подходящего для конкретной задачи стиля программирования. Функциональное программирование в F# заключается в написании четкого и эффективного кода для решения проблем реального программного обеспечения. Оно завязано на такие методики, как применение функций более высокого порядка (higherorderfunctions) и композиция функций (functioncomposition) для создания эффективных и простых в понимании поведений. И оно же состоит в том, чтобы ваш код было легче воспринимать, тестировать и распараллеливать, удаляя скрытые сложности.

Но, чтобы воспользоваться преимуществами всех этих фантастических средств F#, нужно разобраться в основах

Основы функционального программирования

Большинству .NET-разработчиков легче понять функциональное программирование, отталкиваясь от обратного — от того, чем оно не является. Императивное программирование считается стилем, прямо противоположным функциональному. Это также стиль, с которым вы, вероятно, знакомы лучше всего, поскольку большинство мейнстримовых языков программирования являются императивными.

Функциональное и императивное программирование отличаются на фундаментальном уровне, и это можно увидеть даже в таком простейшем коде:

intnumber = 0;

number++;

Очевидно, что здесь значение переменной увеличивается на единицу. Ничего особенного, но взгляните на другой способ решения той же задачи:

constint number = 0;

constint result = number + 1;

Значение number по-прежнему увеличивается на единицу, но оно не модифицируется по месту. Результат сохраняется как другая константа, потому что компилятор не разрешает изменять значение константы. Константы неизменяемы, потому что их значения нельзя модифицировать после определения. И напротив, переменная number в первом примере была изменяемой, так как вы могли модифицировать ее значение. Эти два подхода иллюстрируют одно из фундаментальных различий между императивным и функциональным программированием. В императивном программировании делается упор на использование изменяемых переменных, тогда как в функциональном — на применение неизменяемых значений.

Большинство .NET-разработчиков сказало бы, что number и result в предыдущих примерах являются переменными, но как «функциональный» программист вы должны аккуратнее подбирать слова. В конце концов, одна лишь идея постоянной переменной может просто запутать в лучшем случае. Вместо этого в функциональном программировании говорят, что number и result являются значениями. Термин «переменная» резервируется за объектами, которые можно изменять. Заметьте, что эти термины — не эксклюзив функционального программирования, но они гораздо важнее при программировании в функциональном стиле.

Разница может показаться незначительной, но это фундамент множества концепций, которые и делают функциональное программирование таким эффективным. Изменяемые переменные являются корневой причиной многих неприятнейших ошибок. Как вы увидите, они ведут к неявным зависимостям между различными частями вашего кода, что создает массу проблем, особенно в сочетании с параллельным выполнением. В противоположность этому неизменяемые переменные убирают значительную часть проблем. Они делают возможным применение методик функционального программирования, при котором, например, функции используются как значения, и композиционного программирования, о котором я также расскажу подробнее чуть позже.

Рассмотрим такой пример:

stringstringValue = "world!";

string result = stringValue.Insert(0, "hello ");

Функция Insert формирует строку «helloworld!», но вы знаете, что Insert не модифицирует исходную строку. А все потому, что в .NET строки неизменяемы. Проектировщики .NET Frame­work использовали здесь функциональный подход, так как он упрощает написание более качественного кода, работающего со строками. Поскольку строки — один из самых широко применяемых типов данных в .NET Framework (наряду с другими базовыми типами вроде целых, DateTimes и прочими), все шансы за то, что на самом деле вы шире используете функциональное программирование, чем вам это кажется.

3. F# и Visual Studio 2010

F# поставляется с Visual Studio 2010, и самую последнюю его версию можно найти по ссылке msdn.microsoft.com/vstudio.

F# добавляет в Visual Studio новое окно — F# Interactive, которое позволяет интерактивно выполнять код на F#. Вы можете считать его более мощной версией окна Immediate, доступной даже в том случае, если вы не переключались в режим отладки. Если вы знаете Ruby или Python, то заметите, что F# Interactive выполняет цикл «чтение-оценка-вывод» (Read-Evaluate-PrintLoop, REPL), который является полезным средством для освоения F# и возможностью быстро поэкспериментировать с кодом.

Если вы выделите какой-то код в Visual Studio и нажмете Alt+Enter, то отправите его в F# Interactive. Возьмем простойпример на F#:

let number = 0

let result = number + 1

Запустив этот код в F# Interactive, вы получите следующее:

val number : int = 0

val result : int = 1

Вероятно, вы уже догадались по ключевому слову val, что number и result являются неизменными значениями. Вы можете увидеть это, использовав оператор присваивания (<-) в F#:

>number<- 15;;

 

number<- 15;;

  ^^^^^^^^^^^^

 

stdin(3,1): error FS0027: This value is not mutable

> 

Поскольку вы теперь знаете, что функциональное программирование основано на неизменяемости, эта ошибка должна быть вам понятной. Ключевое слово let используется для создания неизменяемых привязок между именами и значениями. В терминологии C# то же самое можно было сказать так:по умолчанию в F# все является константами. Вы можете сделать переменную изменяемой, но только явным образом. По умолчанию поведение прямо противоположно тому, к чему вы привыкли в императивных языках:

letmutablemyVariable = 0

myVariable<- 15

Логическое распознавание типов и чувствительность к пробелам и отступам

F# позволяет объявлять переменные и значения без указания их типов, поэтому можно подумать, будто F# является динамическим языком, но это не так. Важно понимать, что F# такой же статический язык, как C# или C++. Однако F# имеет мощную систему логического распознавания типов (type inference), которая позволяет во многих местах не указывать типы объектов. Это упрощает синтаксис и делает его лаконичнее, в то же время сохраняя безопасность типов, присущую статическим языкам.

Хотя такие системы логического распознавания типов в действительности отсутствуют в императивных языках, само по себе распознавание типов не связано напрямую с функциональным программированием. Однако распознавание типов — критическая важная концепция, которую нужно понять, если вы хотите освоить F#. К счастью, если вы знаете C#, то наверняка уже знакомы с базовой концепцией логического распознавания типов из-за ключевого слова var:

// Here, the type is explictily given

Dictionary<string, string> dictionary =

new Dictionary<string, string>();

 

// but here, the type is inferred

var dictionary = new Dictionary<string, string>();

Обе строки кода на C# создают новые переменные, которые статически типизируются как Dictionary<string, string>, но во втором случае ключевое слово var сообщает компилятору логически определять тип переменной за вас. F# выводит эту концепцию на новый уровень. Например, вот функция add в F#:

let add x y =

    x + y

 

let four = add 2 2

В приведенном выше коде нет ни одного указания типа, но F# Interactive раскрывает статическую типизацию:

val add : int ->int ->int

val four : int = 4

Смысл стрелок я подробнее объясню потом, а пока вы можете интерпретировать это такункцияadd принимает два int-аргумента, а four является значением типа int. Компилятор F# смог логически распознать все типы, исходя из определений add и four. Компилятор использует при этом правила, которые выходят за рамки данной статьи, но, если вас это интересует, вы можете узнать больше в F# DeveloperCenter.

Логическое распознавание типов — один из способов, с помощью которых F# удаляет лишний «шум» из вашего кода, но обратите внимание еще и на отсутствие фигурных скобок и ключевых слов, обозначающих тело функции add или ее возвращаемое значение. А все дело в том, что F# по умолчанию является языком, чувствительным к пробелам и отступам. В F# вы указываете тело функции простыми отступами, а возвращаемое значение — тем, что оно находится в последней строке функции. По аналогии с распознаванием типов чувствительность к пробелам и отступам не имеет прямого отношения к функциональному программированию, но вы должны быть знакомы с этой концепцией, чтобы пользоваться F#.

Побочные эффекты

Теперь вы знаете, что функциональное программирование отличается от императивного тем, что опирается на неизменяемые значения, а не на модифицируемые переменные, но сам по себе этот факт не слишком полезен. Следующее, в чем нам предстоит разобраться, — побочные эффекты (sideeffects).

В императивном программировании вывод функции зависит от ее входного аргумента и текущего состояния программы. В функциональном — функция зависит только от своих входных аргументов. Иначе говоря, когда вы вызываете функцию более одного раза с одним и тем же входным значением, вы всегда получаете одно и то же выходное значение. Причина, по которой этого нет в императивном программировании, связана с побочными эффектами, как показано на рис. 1.

Рис. 1 Побочные эффекты изменяемые переменных

publicMemoryStreamGetStream() {

var stream = new MemoryStream();

var writer = new StreamWriter(stream);

writer.WriteLine("line one");

writer.WriteLine("line two");

writer.WriteLine("line three");

writer.Flush();

stream.Position = 0;

return stream;

}

 

[TestMethod]

public void CausingASideEffect() {

using (var reader = new StreamReader(GetStream())) {

var line1 = reader.ReadLine();

var line2 = reader.ReadLine();

 

Assert.AreNotEqual(line1, line2);

  }

}

При первом вызове ReadLine поток данных считывается, пока не встретится символ перевода на новую строку. Потом ReadLine возвращает весь текст вплоть до новой строки. Между этими операциями изменяемая переменная, представляющая позицию в потоке данных, обновляется. Это и есть побочный эффект. При втором вызове ReadLine значение этой переменной (Position) уже изменилось, поэтому ReadLine возвращает другое значение.

А теперь рассмотрим одно из самых важных следствий использования побочных эффектов. Во-первых, взгляните на простой класс PiggBank и некоторые методы для работы с ним ( рис. 2).

Рис. 2 ИзменяемыеPiggyBank

public class PiggyBank{

publicPiggyBank(int coins){

    Coins = coins;

  }

 

publicint Coins { get; set; }

}

 

private void DepositCoins(PiggyBankpiggyBank){

piggyBank.Coins += 10;

}

 

private void BuyCandy(PiggyBankpiggyBank){

if (piggyBank.Coins< 7)

throw new ArgumentException(

      "Not enough money for candy!", "piggyBank");

 

piggyBank.Coins -= 7;

}

Если у вас есть свинья-копилка с пятью монетками внутри, вы можете вызвать Deposit­Coins до BuyCandy, но обратное приведет к генерации исключения:

// this works fine

varpiggyBank = new PiggyBank(5);

 

DepositCoins(piggyBank);

BuyCandy(piggyBank);

 

// but this raises an ArgumentException

varpiggyBank = new PiggyBank(5);

 

BuyCandy(piggyBank);

DepositCoins(piggyBank);

ФункцииBuyCandyиDepositCoinsобновляютсостояниесвиньи-копилкичерезпобочныйэффект. Соответственно поведение каждой функции зависит от состояния этой копилки. Поскольку число монеток изменяемое, порядок вызова этих функций имеет значение. Другими словами, между двумя этими методами существует неявная зависимость.

Давайте сделаем число монеток только для чтения, чтобы имитировать неизменяемую структуру данных. На рис. 3 показано, что BuyCandy и DepositCoins теперь возвращают новые объекты PiggyBank вместо обновления существующего PiggyBank.

Рис.3 НеизменяемыеPiggyBank

public class PiggyBank{

publicPiggyBank(int coins){

    Coins = coins;

  }

 

publicint Coins { get; private set; }

}

 

privatePiggyBankDepositCoins(PiggyBankpiggyBank){

return new PiggyBank(piggyBank.Coins + 10);

}

 

privatePiggyBankBuyCandy(PiggyBankpiggyBank){

if (piggyBank.Coins< 7)

throw new ArgumentException(

      "Not enough money for candy!", "piggyBank");

 

return new PiggyBank(piggyBank.Coins - 7);

}

Как и раньше, если вы попытаетесь вызвать BuyCandy до DepositCoins, вы получите исключение:

// still raises an ArgumentException

varpiggyBank = new PiggyBank(5);

 

BuyCandy(piggyBank);

DepositCoins(piggyBank);

Но даже если вы поменяете порядок вызовов на обратный, вы получите тот же результат:

// now this raises an ArgumentException,  too!

varpiggyBank = new PiggyBank(5);

 

DepositCoins(piggyBank);

BuyCandy(piggyBank);

Здесь BuyCandy и DepositCoins зависят только от своих входных аргументов, так как число монеток неизменно. Вы можете выполнять эти функции в любом порядке, и результат будет одним и тем же. Неявная зависимость между функциями исчезла. Однако, если вы хотите успешного выполнения BuyCandy, то должны сделать результат Buy­Candy зависимым от выхода DepositCoins. Вводить такую зависимость нужно явным образом:

varpiggyBank = newPiggyBank(5);

BuyCandy(DepositCoins(piggyBank));

Это тонкое различие с далеко идущими последствиями. Общее изменяемое состояние и неявные зависимости являются источником некоторых из самых трудноуловимых «багов» в императивном коде и причиной, по которой многопоточное программирование столь сложно в императивных языках. Когда приходится заботиться о порядке выполнения функций, вы должны опираться на громоздкие механизмы блокировки, иначе все пойдет наперекосяк. Чисто функциональные программы свободны от побочных эффектов и неявных зависимостей синхронизации, так что порядок выполнения функций не имеет никакого значения. То есть вам не нужно волноваться о механизмах блокировки и прочих, крайне подверженных ошибкам методиках многопоточного программирования.

Простота работы с множеством потоков является основной причиной того, что в последнее время функциональное программирование привлекает повышенное внимание, но у него есть и много других преимуществ. Функции, свободные от побочных эффектов, легче тестировать, поскольку они полагаются только на свои входные аргументы. Они проще в сопровождении, потому что не полагаются неявно на логику из других функций. Функции, свободные от побочных эффектов, также имеют тенденцию к большей компактности и простоте в комбинировании. О последнем я расскажу подробнее чуть позже.

В F# вы оцениваете функции по их значениям результатов, а не по побочным эффектам. В императивных языках функции обычно вызываются для выполнения чего-либо, а в функциональных — для получения некоего результата. Вы можете увидеть это в F#, взглянув на выражение if:

letisEven x =

if x % 2 = 0 then

"yes"

else

        "no"

Вы уже знаете, что в F# последняя строка функции это ее возвращаемое значение, но в этом примере в последней строке функции содержится выражение if. Это не трюк компилятора. В F# даже выражения if спроектированы на возврат значений:

let isEven2 x =

let result =

if x % 2 = 0 then

            "yes"

else

            "no"

result

Значение result имеет тип string и присваивается прямо выражению if. Здесь есть аналогия с тем, как работает условный оператор в C#:

string result = x % 2 == 0 ? "yes" : "no";

Условный оператор возвращает значение, не создавая побочного эффекта. Это более функциональный подход. В противоположность этому выражение if в C# более императивное, так как не возвращает результат. Все, что оно делает, вызывает побочные эффекты.

Композиция функций

Теперь, когда мы увидели некоторые из преимуществ функций, свободных от побочных эффектов, вы готовы задействовать их полный потенциал в F#. Для начала рассмотрим код на C#, который возводит в квадрат числа от 0 до 10:

IList<int> values = 0.Through(10).ToList();

 

IList<int>squaredValues = new List<int>();

 

for (inti = 0; i<values.Count; i++) {

squaredValues.Add(Square(values[i]));

}

Не считая вспомогательных методов Through и Square, этот код вполне стандартен для C#. Хорошие программисты на C#, вероятно, поморщились бы от моего использования цикла for вместо foreach — и были бы правы. Современные языки вроде C# предоставляют циклы foreach в качестве абстракции для упрощения перебора перечислений, избавляя от необходимости явных индексаторов. В этом они преуспели, но взгляните на код на рис. 4.

Рис. 4 Использованиецикловforeach

IList<int> values = 0.Through(10).ToList();

 

// square a list

IList<int>squaredValues = new List<int>();

 

foreach (int value in values) {

squaredValues.Add(Square(value));

}

 

// filter out the even values in a list

IList<int> evens = new List<int>();

 

foreach(int value in values) {

if (IsEven(value)) {

evens.Add(value);

  }

}

 

// take the square of the even values

IList<int> results = new List<int>();

 

foreach (int value in values) {

if (IsEven(value)) {

results.Add(Square(value));

}

}

В этом примере циклы foreach очень похожи, но тело каждого цикла выполняет немного разные операции. Императивные программисты давно привыкли к такому дублированию кода, потому что этот код считается идиоматическим.

Функциональные программисты используют иной подход. Вместо создания абстракций наподобие циклов foreach, помогающих перебирать списки, они применяют функции, свободные от побочных эффектов:

let numbers = {0..10}

letsquaredValues = Seq.map Square numbers

Этот код на F# тоже возводит в квадрат последовательность чисел, но делает это с помощью функции более высокого порядка. Это просто функции, которые принимают в качестве входного аргумента другую функцию. В данном случае функция Seq.map принимает функцию Square в качестве аргумента. Она применяет эту функцию к каждому числу в последовательности и возвращает последовательность чисел, возведенных в квадрат. Именно из функций более высокого порядка многие говорят, что в функциональном программировании функции используются как данные. Это означает лишь то, что функции можно передавать как параметры или присваивать значениям либо переменным точно так же, как типы int или string. В терминологии C# функции более высокого порядка очень сильно напоминают концепцию делегатов и лямбда-выражений.

Применение функций более высокого порядка — одна из методик, которые делают функциональное программирование таким мощным. С их помощью вы можете выделить дублируемый в циклах foreach код и инкапсулировать его в автономные и свободные от побочных эффектов функции. Каждая из этих функций выполняет одну небольшую операцию, которую обрабатывал бы код внутри соответствующего цикла foreach. Поскольку они свободны от побочных эффектов, вы можете комбинировать их для создания более читаемого и простого в сопровождении кода, который делает то же самое, что и циклы foreach:

letsquareOfEvens =

numbers

    |>Seq.filterIsEven

|>Seq.mapSquare

Единственная непонятная часть этого кода — оператор |>. Этот оператор используется для большей читаемости кода, разрешая вам переупорядочивать аргументы в функции, чтобы последний аргумент первым попался вам на глаза. Его определение очень простое:

let (|>) x f = f x

Без оператора |> код squareOfEvens выглядел бы так:

let squareOfEvens2 =

Seq.map Square (Seq.filterIsEven numbers)

Если вы применяете LINQ, то подобное использование функций более высокого порядка должно показаться вам очень знакомым. А все дело в том, что корни LINQ уходят глубоко в функциональное программирование. По сути, вы можете легко адаптировать задачу возведения в квадрат четных чисел под C# с применением методов LINQ:

varsquareOfEvens =

numbers

  .Where(IsEven)

.Select(Square);

Это транслируется в следующий синтаксис LINQ-запроса:

varsquareOfEvens = from number in numbers

whereIsEven(number)

select Square(number);

Использование LINQ в коде на C# или Visual Basic позволяет эксплуатировать некоторые возможности функционального программирования в повседневной работе. И это отличный способ освоения методик такого программирования.

Когда вы начнете регулярно использовать функции более высокого порядка, вы в конце концов придете к такой ситуации, в которой захотите создать небольшую, очень специфическую функцию для передачи в функцию более высокого порядка. Для решения этой задачи «функциональные» программисты используют лямбда-функции. Это просто функции, которые вы определяете, не присваивая имен. Обычно они очень маленькие и имеют сугубо специфическое применение. Например, вот еще один способ, которым вы могли бы возводить в квадрат четные числа, используя лямбду:

letwithLambdas =

numbers

    |>Seq.filter (fun x -> x % 2 = 0)

    |>Seq.map (fun x -> x * x)

Единственное различие между этим и предыдущим кодом заключается в том, что Square и IsEven определены как лямбды. В F# вы объявляете лямбда-функцию ключевым словом fun. Лямбды следует использовать только для объявления функций одного специфического применения, потому что их сложно задействовать вне области видимости, в которой они были определены. По этой причине Square и IsEven — плохой выбор на роль лямбда-функций, так как они полезны во многих ситуациях.

Карринг и частичное применение

Теперь вы знаете почти все основы, необходимые для того, чтобы приступить к работе с F#, но есть еще одна концепция, которую вы должны освоить. В предыдущих примерах оператор |> и стрелки в сигнатурах типов из F# Interactive тесно связаны с концепцией так называемогокарринга.

Карринг (currying) — это разбиение функции со множеством аргументов на серию функций, каждая из которых принимает один аргумент, и в конечном счете они дают тот же результат, что и исходная функция. Карринг, по-видимому, является в этой статье самой трудной концепцией для .NET-разработчика, особенно из-за того, что его часто путают с частичным применением (partialapplication). Работу обеих концепций иллюстрирует следующий пример:

letmultiply x y =

x * y

 

let double = multiply 2

letten = double 5

Прямо сейчас вы должны увидеть поведение, отличающееся от того, которое вы наблюдали бы в большинстве императивных языков. Второе выражение создает новую функцию double передачей одного аргумента в функцию, которая принимает два. Результат — функция, которая принимает один аргумент типа int и возвращает то же самое, как если бы вы вызвали multiply с x, равным 2, и y, равным этому аргументу. По своему поведению этот код эквивалентен:

let double2 z = multiply 2 z

Зачастую в таком случае ошибочно полагают, что multiply разбивается до double. Но это верно лишь отчасти. Функция multiply подвергается каррингу, однако это происходит, когда она определена, так как в F# функции подвергаются каррингу по умолчанию. Когда создается функция double, точнее сказать, что функция multiply применяется частично.

Давайте пройдем все этапы в деталях. Еще раз повторю, что при карринге функция с множеством аргументов разбивается на серию функций с одним аргументом, которые в конечном счете дают тот же результат, что и исходная. Функция multiply имеет следующую сигнатуру типа согласно F# Interactive:

valmultiply :int ->int ->int

К этому моменту мы расшифровали, что multiply является функцией с двумя аргументами типа int и возвращаемым значением того же типа. Теперь я поясню, что происходит на самом деле. Функция multiply в действительности состоит из двух функций. Первая принимает один аргумент типа int и возвращает другую функцию, в конечном счете связывающую x с конкретным значением. Эта функция также принимает один аргумент типа int, которое можно считать значением, связываемым с y. После вызова этой второй функции x и y связаны со своими значениями, поэтому результатом является произведение x на y, как определено в теле функции double.

Чтобы создать double, первая функция в цепочке функций multiply оценивается как частично применяемая multiply. Полученной функции присваивается имя double. Когда оценивается double, она использует свой аргумент вместе с частично примененным значением для создания результата.

Использование F# и функционального программирования

Теперь, когда у нас есть достаточный словарь для того, чтобы начать работу с F# и окунуться в функциональное программирование, перед вами открывается уйма вариантов, что делать дальше.

Окно F# Interactive позволяет исследовать код на F# и быстро создавать сценарии (scripts) F#. Оно также полезно для рутинной проверки поведения библиотечных функций .NET без обращения к справочным файлам или поиску в Интернете.

F# превосходен в выражении сложных алгоритмов, поэтому вы можете инкапсулировать соответствующие части кода своего приложения в F#-библиотеки, чтобы потом вызывать их из других .NET-языков. Это особенно полезно в инженерных или параллельных приложениях.

Наконец, вы можете использовать методики функционального программирования в своей повседневной разработке для .NET даже без написания кода на F#. Просто используйте LINQ вместо циклов for или foreach. Попробуйте применять делегаты для создания функций более высокого порядка. Ограничьте себя в использовании изменяемости и побочных эффектов в своих императивных языках. Как только вы начнете писать код в функциональном стиле, вы очень скоро захотите создавать больше кода на F#.       

 


 

Тема 3. Функциональное программирование на языке Haskell

Haskell - строго типизированный язык. Это означает, что любая конструкция в языке, задающая некоторый объект, имеет определенный тип, который можно «вычислить» еще до начала выполнения программы, то есть статически. Таким же свойством обладают большинство современных языков программирования, такие как Паскаль, Java, Си++ и многие другие. Труднее привести пример не строго типизированного языка (если, конечно, не считать некоторых отступлений от строгой типизации в упомянутых выше языках, сделанных для удобства практического программирования, таких, например, как тип Pointer в языке Паскаль). Пожалуй, самым известным из языков, изначально не имеющих строгой типизации, является первый язык функционального программирования Лисп.

Основу системы типов языка Haskell составляют элементарные встроенные типы данных: целые, представленные двумя подтипами с идентификаторами Integer и Int, вещественные, также с двумя подтипами Float и Double, логические, имеющие идентификатор типа Bool и символьные (Char). Заметим сразу же, что все идентификаторы в

 

Haskell чувствительны к регистру букв, так что integer, Integer и INTEGER – это три разных идентификатора. Регистр первой буквы идентификатора определяет также, к какому из двух «миров» относится идентификатор: если идентификатор начинается с заглавной буквы, то он обозначает встроенный или определенный программистом тип или класс. Идентификаторы объектов – значений простых и сложных типов, в том числе функций – начинаются со строчной буквы или символа подчеркивания. Идентификаторы, как и в других языках программирования, строятся из букв, подчеркиваний и цифр, однако в Haskell допустимо использовать для построения идентификаторов также символ "'" («апостроф»). Ни апостроф, ни цифра не могут быть первым символом идентификатора. Заметим, что апостроф традиционно используется для построения имен функций, вспомогательных по отношению к функции, заданной тем же именем, только без апострофов. Например, если мы определяем функцию с именем factorial и в качестве вспомогательных для нее хотим определить еще две функции, не имеющие самостоятельного значения, то имена этих функций по традиции, скорее всего, будут factorial' и factorial''.

Четыре базовых типа - это, конечно, типы, определяющие наборы хорошо известных значений целого, вещественного, логического и символьного типов. Отметим несколько особенностей базовых типов Haskell, непривычных по другим языкам программирования.

Тип Integer определяет потенциально бесконечный набор целых чисел произвольной длины. В операциях над целыми типа Integer никогда не происходит «переполнения» (конечно, в пределах выделенной для работы программы памяти). Для повышения эффективности работы программ можно использовать и вполне традиционные целые ограниченной длины. Для таких «ограниченных» целых используется идентификатор типа Int. Последовательность цифр, возможно, предваренная знаком '–', представляет в программе целое число (как говорят, целые числа имеют литеральные обозначения в виде последовательности цифр).

Вещественные числа типов Float и Double определяются вполне традиционно и имеют также традиционные литеральные обозначения, такие как 3.14, -2.71828 или 0.12е-10.

Символьный тип также имеет литеральные обозначения для своих значений, и они тоже вполне традиционны. Так, обозначение 'а' представляет символ а.

Логический тип представлен двумя значениями – истина и ложь. Литеральных обозначений для логических значений в языке нет, однако, имеются два стандартных идентификатора – True и False, обозначающие истинное и ложное значения соответственно.

 

В программе можно вводить свои собственные идентификаторы для имеющихся значений. Такая возможность совершенно эквивалентна средствам описания констант в различных языках программирования. Например, идентификатор school можно ввести для обозначения константы 239 с помощью следующего фрагмента программы:

school = 239

Если мы хотим явно указать тип для нового идентификатора объекта, то это можно сделать с помощью конструкции определения типа. Например, если мы хотим указать, что введенный идентификатор school должен представлять значение типа Integer, то перед определением значения следует написать school :: Integer

Такое уточнение типа не обязательно, потому что система программирования на языке Haskell обязана сама устанавливать (выводить) типы всех встречающихся объектов и выражений, но иногда это может все же оказаться существенным. Так, например, литерал 239 в предыдущем примере может обозначать как целое типа Int, так и целое типа Integer в зависимости от контекста. Явное указание типа Integer устраняет неоднозначность.

Значения произвольных типов можно объединять в кортежи. Аналогом этого понятия в языке Паскаль будет определение типа записи. Объединяемые в кортеж значения просто перечисляются в скобках через запятую. Например, обозначение (2, 'a') представляет кортеж из двух значений - целого числа 2 и символа 'а'. Заметим, что типом этого кортежа будет (Int, Char), так что можно написать следующий фрагмент программы, в котором вводится новое обозначение для этого кортежа и явно указывается его тип:

pair :: (Int, Char) pair = (2, 'a')

Разумеется, кортежи сами могут входить в состав других кортежей. Например, следующий фрагмент программы определяет идентификатор для кортежа, в состав которого входит другой кортеж.

myValue :: (Int, (Char, Char), Bool) myValue = (366, ('A', 'K'), True)

Над значениями можно выполнять определенные в языке операции и применять стандартные функции. Полный набор стандартных функций можно посмотреть, например, в официальном пересмотренном сообщении о языке Haskell'98, или в других подробных документах по языку. Мы будем постепенно вводить новые операции и стандартные функции по мере необходимости, а сейчас пока отметим, что в языке имеется достаточно обычный набор арифметических и логических операций, а также операций сравнения и функций преобразования значений из одних

 

типов в другие. Запись выражений также достаточно традиционна, так что не вызывает никакого удивления, что, скажем, выражение 2+3 записано правильно и при вычислении выдает значение 5, а выражение 3<(2+2) выдает значение истина. Наиболее необычными являются две особенности записи выражений.

Во-первых, при вызове функций аргументы отделяются от идентификатора функции не привычными скобками, а пробелом. Если аргументов несколько, то сами аргументы тоже отделяются друг от друга пробелами. Так, например, функцию вычисления синуса, определенную для вещественных аргументов, можно вызвать с помощью конструкции sin 0.5

а функцию определения максимального из двух любых упорядоченных значений  можно  вызвать    предположении,  что  идентификатор  x обозначает некоторое целое значение), скажем, так: max 3 (x+1)

Вторая существенная особенность состоит в том, что операторы, задающие арифметические и другие операции, вообще говоря, синтаксически неотличимы от обычных двуместных функций. Я имею в виду, что вызов операции, подобной сложению двух целых, можно записывать не только в виде бинарной операции, расположенной между аргументами, но и так, как это принято для обычных двуместных функций. Для превращения знака бинарной операции в двуместную функцию надо лишь заключить знак операции в скобки, так что запись

(+) x y

полностью эквивалентна записи

x + y

Обратное тоже верно. Для того, чтобы превратить обычный идентификатор функции двух аргументов в бинарный оператор достаточно заключить идентификатор функции в обратные апострофы, так что приведенное выше обращение к функции вычисления максимального из двух упорядоченных значений можно было бы записать в виде применения бинарной операции:

3 `max` (x+1)

Конечно, для того, чтобы написать хоть сколько-то полезную программу, необходимо помимо новых идентификаторов для объектов и написания выражений уметь самому определять новые функции. Каждая функция в Haskell – это тоже некоторое значение, для которого определен тип. В типе функции указывают типы ее аргументов и тип значения функции, при этом типы аргументов и значения функции отделяются друг от друга символом '->', так что, например, тип функции одного вещественного аргумента с вещественным результатом (такой, как, например, функция вычисления синуса) можно записать в виде

Double -> Double

а тип функции с двумя целыми аргументами и одним логическим результатом (такой, как, например, операция сравнения двух целых значений по величине) можно записать в виде

Int -> Int -> Bool

Саму функцию можно определить с помощью «уравнения», в котором определяется, как функция должна себя вести, если ей задать значение аргумента. Давайте, например, определим функцию удвоения, которая удваивает значение своего вещественного аргумента и выдает получившееся значение. Для этого зададим идентификатор функции twice, зададим ее тип и напишем уравнение, в котором покажем, что вызов этой функции с заданным значением аргумента эквивалентен выражению, в котором это значение умножается на два:

twice :: Double -> Double twice x = 2 * x

В уравнении, определяющем поведение функции twice, можно выделить левую часть, задающую форму вызова функции, и правую часть, в которой записывается выражение, заменяющее вызов функции в тот момент, когда будет задано значение ее аргумента. Если функция twice определена, то вычисление выражения twice 5.5 можно представить себе следующим образом. Сначала происходит сопоставление фактического значения аргумента 5.5 с формальным аргументом функции, заданном в уравнении, определяющем поведение функции. Потом вызов функции заменяется правой частью уравнения, в которой вместо формального аргумента используется сопоставленное с ним значение фактического аргумента. Таким образом, после сопоставления и замены вместо выражения twice 5.5 получаем выражение 2 * 5.5, которое после вычисления (применения операции умножения) дает значение 11. Процесс преобразования (вычисления) выражения можно записать следующим образом:

twice 5.5     2 * 5.5     11

Преобразование выражений, подобное только что приведенному, называют еще редукциями. Таким образом, можно сказать, что вычисление выражений в Haskell (исполнение программы) осуществляется с помощью последовательных редукций исходного выражения.

Определение функции может быть рекурсивным, то есть в правой части уравнения может быть вызов определяемой функции. В этом случае в процессе преобразования выражения, содержащего вызов рекурсивной функции, может получаться выражение, также содержащее вызов той же самой функции. Для того, чтобы процесс вычисления мог закончиться, необходимо использовать условные выражения, которые приводят к

выбору одной из двух альтернатив при вычислении сложных выражений. Условное выражение имеет вид

if <условие> then <выражение-"то"> else <выражение-"иначе">

При вычислении условного выражения прежде всего вычисляется условие. Тип вычисленного выражения-условия должен быть логическим (Bool). Если вычисленное значение оказывается истинным, то вместо всего условного выражения подставляется выражение-"то", а выражение­"иначе" отбрасывается. Если же наоборот, условие оказалось ложным, то отбрасывается выражение-"то", а вместо всего условного выражения остается только часть, определенная выражением-"иначе".

Зададим определение простой рекурсивной функции, предназначенной для вычисления факториала заданного целого числа. Простое и естественное определение этой функции может выглядеть следующим образом:

factorial :: Integer -> Integer

factorial n = if n == 0 then 1 else n * factorial (n-1)

Проследим   за   тем,   как   происходит   вычисление   выражения

factorial 3, записывая последовательно все этапы преобразования

выражения в соответствии с определением функции factorial (листинг

1.6).

Листинг 1.6. Процесс преобразования выражения при вычислении факториала числа

factorial 3                                                                                                      

if 3 == 0 then 1 else 3 * factorial 2                                     

if False then 1 else 3 * factorial 2                                       

3 * factorial 2                                                                                            

3 * if 2 == 0 then 1 else 2 * factorial 1                           

3 * 2 * factorial 1                                                                                  

3 * 2 * if 1 == 0 then 1 else 1 * factorial 0                 

3 * 2 * 1 * factorial 0                                                                        

3 * 2 * 1 * if 0 == 0 then 1 else 0 * factorial (-1)

3 * 2 * 1 * 1                                                                                                   

6

Вместо использования условного выражения можно записать несколько уравнений, определяющих поведение функции при различных значениях аргумента. Так, например, можно переопределить функцию вычисления факториала, задав для нее два уравнения, обусловленных выражениями-«сторожами», определяющими, какое из двух уравнений на самом деле следует применять.

factorial :: Integer -> Integer

factorial n | n == 0 = 1

factorial n | n > 0 = n * factorial (n-1)

 

или короче

factorial :: Integer -> Integer factorial n | n == 0 = 1

| n > 0 = n * factorial (n-1)

Обратите внимание, что в новом варианте функции сторожа не покрывают всех возможных значений аргумента. Если написать бессмысленный вызов функции factorial -3, то при нашем первоначальном определении вычисление выражения в программе уйдет в бесконечный цикл; преобразование выражения никогда не сможет нормально закончиться. Во втором случае система программирования сразу же выдаст сообщение об ошибке, поскольку ей не удастся найти уравнение со сторожем, который при вычислении выдаст истинное значение.

Еще один способ определения той же функции состоит в том, что в уравнениях, определяющих функцию, можно сразу же задать случай, когда аргументом вызова будет нулевое значение. Определение функции теперь может выглядеть так:

factorial :: Integer -> Integer

factorial 0 = 1

factorial n = n * factorial (n-1)

В этом определении тоже происходит выбор нужного уравнения из двух возможных вариантов. Порядок задания уравнения здесь важен. Сначала рассматривается первое уравнение, в котором фактический аргумент сопоставляется с нулевой константой. Сопоставление возможно только в том случае, когда сам этот аргумент равен нулю. Если же аргумент больше нуля, то будет выбрано второе уравнение, в котором сопоставление всегда произойдет успешно, поскольку с идентификатором n может сопоставиться любое значение. После замены выражения вызова на правую часть будет произведено новое сопоставление и выбор уравнения и т.д. Вычисление факториала будет, конечно, происходить по тому же самому алгоритму. Заметим, что в этом последнем способе задания функции опять возможным становится задание отрицательного значения аргумента, при этом программа будет выполняться бесконечно, а никакого сообщения об ошибке выдано не будет.

Напишем еще несколько функций, выполняющих те или иные арифметические действия, просто для того, чтобы освоиться с манерой написания функций в языке Haskell. Определим функцию, которая по заданным натуральным числам определяет их наибольший общий делитель согласно алгоритму Евклида. Назовем эту функцию gcd (greatest common divider). Идея Евклида состоит в том, что если ни одно из чисел не равно нулю, то наибольший общий делитель таких двух чисел равен наибольшему общему делителю наименьшего из них и остатка от деления большего на меньшее. Поскольку остаток от деления одного числа на

другое можно вычислить с помощью стандартной бинарной операции mod, то функцию gcd можно определить следующим образом (листинг 1.7):

Листинг 1.7. Наибольший общий делитель двух чисел

gcd :: Integer -> Integer -> Integer -- в первом уравнении обеспечиваем, -- чтобы первый аргумент был больше второго. -- функция определена только для неотрицательных аргументов gcd m n | m < 0 = error "gcd: Negative argument" | n < 0 = error "gcd: Negative argument" -- собственно алгоритм Евклида: gcd m 0 = m gcd m n = gcd n (m `mod` n)

В этом примере использованы различные виды уравнений, определяющих одну и ту же функцию. Стандартная функция error выводит в стандартный выходной поток сообщение, являющееся ее аргументом и возвращает в качестве результата «неопределенное значение». Если такое неопределенное значение действительно будет получено в результате работы программы, то программа будет завершена аварийно. Как и в только что показанном примере функция error обычно используется для вывода диагностических сообщений и аварийного завершения работы программы в случае неверно заданных аргументов функции.


 

 

Тема 4. "Искусственный интеллект".

1.                Понятие Основные термины и определения.

2.                Предпосылки и история развития ИИС.

3.                Правила формулировки условий задач и выбор модели решения.

 

1. Понятие "искусственный интеллект". Основные термины и определения.

Понятие "искусственный интеллект" многогранно, и разные авторы дают разные определения.

Искусственный интеллект – это метафорическое понятие для обозначения системы созданных людьми средств, воспроизводящих определенные функции человеческого мышления. (Философский словарь)

Искусственный интеллект – это искусственная система, имитирующая решение человеком сложных задач. (Энциклопедический словарь по информатике)

Определение, которого мы будем придерживаться в нашем курсе: Искусственный интеллект – это программная система, имитирующая на компьютере мышление человека.

Программы, реализующие элементы искусственный интеллекта, называют интеллектуальными информационными системами (ИИС).

Система считается интеллектуальной, если в ней реализованы следующие три базовые функции.

1.                Функция представления и обработки знаний. Интеллектуальная система должна быть способна накапливать знания об окружающем мире, классифицировать и оценивать их с точки зрения прагматики и непротиворечивости, инициировать процессы получения новых знаний, соотносить новые знания со знаниями, хранящимися в базе знаний.

2.                Функция рассуждения. Интеллектуальная система должна быть способна формировать новые знания с помощью логического вывода и механизмов выявления закономерностей в накопленных знаниях, получать обобщенные знания на основе частных знаний и логически планировать свою деятельность.

3.                Функция общения. Интеллектуальная система должна быть способна общаться с человеком на языке, близком к естественному языку (ЕЯ) и получать информацию через каналы, аналогичные тем, которые использует человек при восприятии окружающего мира (прежде всего, зрительный и звуковой). Также система должна уметь формировать «для себя» или по просьбе человека объяснения собственной деятельности (т. е. отвечать на вопросы типа «Как я это сделал?»), оказывать человеку помощь за счет знаний, которые хранятся в ее памяти, и логических средств рассуждения.

Основная цель ИИС – это реализовать информационный процесс максимально приближено к мыслительному процессу человека. Целями интеллектуальных информационных технологий являются, во-первых, расширение круга задач, решаемых с помощью компьютеров, особенно в слабоструктурированных предметных областях, и, во-вторых, повышение уровня интеллектуальной информационной поддержки современного специалиста.

Так как интеллект присущ только человеку, а интеллект – это есть совокупность знаний и навыков их обработки, то для создания ИИС мыслительные процессы формализуются, раскладываются на простейшие операции и их связки, а затем программируются исходя из особенностей задачи и выбранного языка программирования.

 

2. Предпосылки и история развития ИИС.

Искусственный интеллект (ИИ) как научное направление, связанное с попытками формализовать мышление человека, имеет длительную историю. Еще Платон, Аристотель, Р. Декарт, Г.В. Лейбниц, Дж. Буль и многие другие исследователи на уровне современных им знаний стремились описать мышление как совокупность некоторых элементарных операций, правил и процедур. Перечислим самые значимые вехи в эволюции научной мысли, оказавшие влияние на развитие идей интеллектуальных информационных систем. В начале XVII века Рене Декарт предположил, что животное – некий сложный механизм, тем самым сформулировав механистическую теорию. В 1910-1913 гг. Бертран Рассел и А. Н. Уайтхед опубликовали работу «Принципы математики», которая произвела революцию в формальной логике. В 1941 Конрад Цузе построил первый работающий программно-контролируемый компьютер. Качественно новый период развития искусственного интеллекта связан с появлением в научных лабораториях ЭВМ и с публикацией книги Н. Винера "Кибернетика или управление и связь в животном и машине" появившейся еще ранее книги У. Росс Эшби "Введение в кибернетику".УорренМаккалок

 и ВалтерПиттс в 1943 опубликовали A Logical Calculus of the Ideas Immanentin Nervous Activity, который заложил основы нейронных сетей.

В нашей стране идеи создания ИИ получили признание после выхода целой серии переводных работ Н. Винера, У. Росс Эшби, Ст. Бира и Э. Беркли в конце 1950-х – начале 1960-х годов прошлого века и нашего соотечественника Берга.

Большинство работ на этапе зарождения систем искусственного интеллекта было посвящено исследованию моделирования рассуждений, это одно из самых развитых направлений. Моделирование рассуждений подразумевает создание символьных систем, на входе которых поставлена некая задача, а на выходе требуется её решение. Как правило, предлагаемая задача уже формализована, то есть переведена в математическую форму, но либо не имеет алгоритма решения, либо он слишком сложен, трудоёмок и т. п. В это направление входят: доказательство теорем, принятие решений и теория игр, планирование и диспетчеризация, прогнозирование.

Также весьма развитым направлением, которому придаётся большое значение, является обработка естественного языка, в рамках которого проводится анализ возможностей понимания, обработки и генерации текстов на «человеческом» языке. Первоначально это направление связано с проблемой качественного машинного перевода текстов с одного языка на другой. С развитием информационных технологий, и в частности Internet, большое значение получила разработка методов информационного поиска.

В последние годы на первый план выходит инженерия знаний, объединяющая задачи получения знаний из простой информации, их систематизации и использования. Это направление охватывает большой спектр задач, самым важными являются две из них, решению которых посвящено большое количество теоретических и практических работ. Первая – машинное обучение – касается процесса самостоятельного получения знаний интеллектуальной системой в процессе её работы. Вторая – создание экспертных систем – программ, использующих специализированные базы знаний для получения достоверных заключений по какой-либо проблеме.

Большие и интересные достижения имеются в области моделирования биологических (имеющих биологические аналоги) систем. Нейронные сети используются для решения нечётких и сложных проблем, таких как распознавание геометрических фигур или кластеризация объектов. Генетический подход основан на идее, что некий алгоритм может стать более эффективным, если позаимствует лучшие характеристики у других алгоритмов («родителей»). Относительно новый подход, где ставится задача создания автономной программы — агента, взаимодействующего с внешней средой, называется агентным подходом, одним из подтипов которого является "муравьиный интеллект", когда совокупность агентов с "низким" интеллектом образует высокоинтеллектуальную систему.

На стыке робототехники и искусственного интеллекта также возникает ряд интересных задач, направленных на создание интеллектуальных роботов. Сюда, в частности входят, задачи распознавания образов: текстов, звуков, символов и компьютерное зрение.

Отдельным направлением является "машинное творчество", здесь поставлены проблемы написания компьютером музыки, литературных произведений (стихов, сказок), художественное творчество.

Огромное количество практических приложений, таких как, программирование интеллекта в компьютерных играх, нелинейное управление, интеллектуальные системы безопасности, порождает отдельные в исследованиях искусственного интеллекта. Таким образом, эта наука продолжает интенсивно развиваться, появляются новые направления исследований, разрабатываются прикладные задачи и приложения.

Пока ни одна исследовательская группа ни в одном из направлений не подошла в плотную к  воплощению искусственного интеллекта на практике, но исследования в области искусственного интеллекта влились в общий поток технологий сингулярности (видового скачка), таких как нанотехнология, молекулярная биоэлектроника, квантовая теория и т. д.

Приведём примеры наиболее впечатляющих систем искусственного интеллекта, реализованных на практике, информация о которых не является засекреченной:

-                    DeepBlue – суперкомпьютер со специальной архитектурой и ПО – победил чемпиона мира по шахматам. Оригинальные компактные шахматные программы прочно завоевали рынок интеллектуальных игр. На основе этой линий суперкомпьютеров IBM были созданы системы молекулярное моделирование и моделирование системы пирамидальных клеток в швейцарском центре BlueBrain.

-                    Mycin — одна из ранних экспертных систем, которая могла диагностировать небольшой набор заболеваний, причем часто так же точно, как и доктора. Получила большую популярность, стала примером грамотного воплощения идей искусственного интеллекта на практике.

-                    ViaVoice – система распознавания речи – способна обслуживать потребителей. В последнее время появляется большое количество коммерческих аналогов.

-                    Роботы в ежегодном турнире RoboCup соревнуются в упрощённой форме футбола. Самым известным является робот японских производителей Akme.

Банки применяют системы искусственного интеллекта в страховой деятельности (актуарная математика) при игре на бирже и управлении собственностью. В августе 2001 года роботы выиграли у людей в импровизированном соревновании по трейдингу. Методы распознавания образов, (включая, как более сложные и специализированные, так и нейронные сети) широко используют при оптическом и акустическом распознавании (в том числе текста и речи), медицинской диагностике, спам-фильтрах, в системах ПВО (определение целей), а также для обеспечения ряда других задач национальной безопасности.

Разработчики компьютерных игр вынуждены применять ИИ той или иной степени проработанности. Стандартными задачами ИИ в играх являются нахождение пути в двухмерном или трёхмерном пространстве, имитация поведения боевой единицы, расчёт верной экономической стратегии и так далее.

На текущем этапе развития информационных технологий намечены 2 стратегических направления развития систем искусственного интеллекта:

1.                решение проблем, связанных с приближением специализированных систем ИИ к возможностям человека и их интеграции, которая будет реализована на основе закономерностей природы человека;

2.                создание искусственного разума, представляющего интеграцию уже созданных систем ИИ в единую систему, способную решать проблемы человечества.

 

3. Правила формулировки условий задач и выбор модели решения.

При проектировании модели представления знаний следует учитывать такие факторы, как однородность представления и простота понимания. Однородное представление приводит к упрощению механизма управления логическим выводом и упрощению управления знаниями. Представление знаний должно быть понятным экспертам и пользователям системы. А противном случае затрудняются приобретение знаний и их оценка. Однако выполнить это требование в равной степени как для простых, так и сложных задач довольно трудно. Обычно для несложных задач останавливаются на некотором среднем (компромиссном) представлении, но для решения сложных и больших задач необходимы структурирование и модульное представление.

В современной теории интеллектуальных информационных систем выделены девять ключевых требований к эффективным моделям представления знаний:

1.                общность (универсальность);

2.                «психологичность», наглядность представления знаний;

3.                однородность;

4.                реализация в модели свойства активности знаний;

5.                открытость базы знаний;

6.                возможность отражения в базы знаний структурных отношений объектов предметной области;

7.                наличие механизма «проецирования» знаний на систему семантических шкал;

8.                возможность оперирования нечеткими знаниями;

9.                использование многоуровневых представлений (данные, модели, метамодели и т.д.).

 


 

Тема 5. Представление знаний фреймами и выводы.

1.                Основы теории фреймов.

2.                Свойства и основные параметры фреймов.

 

1. Основы теории фреймов.

Для фреймовой модели характерно представление знаний сравнительно большими единицами информации, использование иерархической структуры зависимостей единиц информации, определяемой уровнем абстракции, а также возможность комбинации декларативных и процедурных подходов при обработке информации. Чтобы наиболее полно использовать возможности фреймовой модели вводятся специальные языки представления данных, которые позволяют, как исследовать представление знаний, так строить многоцелевые базы знаний. Такой подход особенно актуален в САПР (системах автоматического проектирования).

Теория фреймов относится к психологическим понятиям, касающимся понимания того, что мы видим и слышим. Она была разработана и опубликована М.Минским в 1975 г. Эта теория основывается на следующих основных правилах:

1.                Фрейм – это единица представления знаний созданная (или запомненная) на основе прошлых данных. Каждый фрейм может быть дополнен различной информацией в зависимости от ситуации его использования.

2.                Фрейм представляет собой сеть из нескольких вершин и отношений, их соединяющих. На самом верхнем уровне фрейма представлена фиксированная информация: истинные факты, касающиеся объекта. На последующих расположены объединения фактов, на основе правил соответствия (которые задаются на уровне фрейма) – так называемые терминальные слоты или терминалы. Таким образом, каждый фрейм представляет собой иерархическую структур отношений типа "абстрактное - конкретное".

3.                Обычно каждый терминал содержит не только данные, но и процедуры позволяющие обрабатывать эти данные, а также передавать результаты другим терминалам.

4.                Каждый фрейм содержит специальную процедуру вывода, которая позволяет ему выполнять запросы к другим пользователям (или пользователю).

5.                Несколько терминалов каждого фрейма обязательно заранее определяются и заполняются значениями по умолчанию. Это обеспечивает информативность фрейма даже в случае отсутствия конкретных данных, а также позволяет изменить целевую направленность фрейма, просто заменив данные по умолчанию. Также подобные данные могут использоваться для введения логических ограничений в систему.

 

2. Свойства и основные параметры фреймов.

На основе фреймов могут быть составлены информационно-поисковые сети. Фрейм-кандидат исследуется на соответствие данной проблеме, если он не отвечает требованиям, то осуществляется переход к следующему фрейму по специальной метке. При этом информация согласования может пополнять фрейм.

К основным свойствам фреймов относят следующие:

1.                Базовый тип. В качества базовых фреймов запоминаются только важнейшие свойства объекта, которые являются постоянными для большинства объектов данного типа.

2.                Имя фрейма. Это идентификатор, присваиваемый фрейму, фрейм должен иметь имя, единственное в данной фреймовой системе (уникальное имя).

3.                Имя слота. Это идентификатор, присваиваемый слоту; слот должен иметь уникальное имя во фрейме, к которому он принадлежит. Каждый фрейм состоит из произвольного числа слотов, причем несколько из них обычно определяются самой системой для выполнения специфических функций, а остальные определяются пользователем. Обычно имя слота не несет никакой смысловой нагрузки и является лишь идентификатором данного слота, но в некоторых случаях оно может иметь специфический смысл. К таким именам помимо IS-А (отношение IS-А), DDESENDANTS (указатель прямого дочернего фрейма), FINEDBY (пользователь, определяющий фрейм), DEFINEDON (дата определения фрейма), MODIFIEDON (дата модификации фрейма), COMMENT (комментарий) и т. п. относятся имена, используемые для представления структурированных объектов, например HASPART, RELATIONS и другие. Эти слоты называются системными и используются при редактировании базы знаний и управлении выводом.

4.                Указатели наследования. Эти указатели касаются только фреймовых систем иерархического типа, основанных на отношениях “абстрактное – конкретное”, они показывают, какую информацию об атрибутах слотов во фрейме верхнего уровня наследуют слоты с такими же именами во фрейме нижнего уровня. Типичные указатели наследования Unique (U: уникальный), Same (S: такой же), Range (R: установление границ), Override (O: игнорировать) и т. п.

5.                Указание типа данных. Указывается, что слот имеет численное значение, либо служит указателем другого фрейма (т.е. показывает имя фрейма). К типам данных относятся FRAME (указатель), INTEGER (целый), REAL (действительный), BOOL (булев), LISP (присоединенная процедура), ТЕХТ (текст), LIST (список), TABLE (таблица), ЕXРRESSION (выражение) и другие.

6.                Значение слота. Пункт ввода значения слота. Значение слота должно совпадать с указанным типом данных этого слота, кроме того, должно выполняться условие наследования.

7.                Демон. Здесь дается определение демонов типа IF-NEEDED, IF-ADDED, IF-REMOVED и т. д. Демоном называется процедура, автоматически запускаемая при выполнении некоторого условия. Демоны запускаются при обращении к соответствующему слоту. Например, демон IF-NEEDED запускается, если в момент обращения к слоту его значение не было установлено, IF-ADDED запускается при подстановке в слот значения, IF-REMOVED запускается при стирании значения слота. Кроме того, демон является разновидностью присоединенной процедуры.

8.                   Присоединенная процедура. В качестве значения слота можно использовать программу процедурного типа, называемую служебной (servant) (в языке Лисп) или методом (в языке Смолток). В данном случае присоединенная процедура запускается по сообщению, переданному из другого фрейма. Когда мы говорим, что в моделях представления знаний фреймами объединяются процедурные и декларативные знания, то считаем демоны и присоединенные процедуры процедурными знаниями. Кроме того, в языке представления знаний фреймами отсутствует специальный механизм управления выводом, поэтому пользователь должен реализовать данный механизм с помощью присоединенной процедуры. Следовательно, язык представления знаний фреймами можно назвать языком, ориентированным на специалистов по искусственному интеллекту, а также языком, ориентированным на сложные прикладные проблемы.


 

Тема 6. Семантические сети и онтологии.

План:

1.     Понятие семантической сети.

2.     Онтологии.

3.     Семантическая паутина.

 

1. Понятие семантической сети.

Семантические сети разрабатывались как модель представления структуры долговременной памяти в психологии. Сейчас они используются для представления знаний в инженерии знаний.

Семантическая сеть – информационная модель предметной области, имеющая вид ориентированного графа, вершины которого соответствуют объектам предметной области, а дуги (рёбра) задают отношения между ними. Объектами могут быть понятия, события, свойства, процессы.

Компьютерные семантические сети были детально разработаны Ричардом Риченсом в 1956 году в рамках проекта Кембриджского центра изучения языка по машинному переводу. Процесс машинного перевода подразделяется на 2 части: перевод исходного текста в промежуточную форму представления, а затем эта промежуточная форма транслируется на нужный язык. Такой промежуточной формой как раз и были семантические сети.

Одной из разновидностей семантической сети, получившей большое распространение, является модель TLC (Teachable Language Comprehended – доступный механизм понимания языка), предложенная Куиллианом и носящая его имя. В данной модели для описания структуры долговременной памяти была использована сетевая структура, в которой особое внимание уделяется понятию "класс". Класс ассоциируется с различными свойствами и примерами, что позволяет точно определять значение понятия. Причём определение каждого объекта происходит в своей понятийной плоскости (как предметной, так и уровневой), переход с одной плоскости на другую позволяет уточнить или даже видоизменить понятие объекта. Дана модель используется в качестве базисной в компьютерных онтологиях.

Для семантических сетей используют следующие классификации:

По количеству типов, сети могут быть однородными и неоднородными. Однородные сети обладают только одним типом отношений (стрелок), например, классификация биологических видов. В неоднородных сетях количество типов отношений больше двух. Неоднородные сети представляют больший интерес для практических целей, но и большую сложность для исследования.

По арности, типичными являются сети с бинарными отношениями (связывающими ровно два понятия). Бинарные отношения очень просты и удобно выглядят на графе в виде стрелки между двух концептов. Кроме того, они играют исключительную роль в математике. На практике, однако, могут понадобиться отношения, связывающие более двух объектов – N-арные. При этом возникает сложность – как изобразить подобную связь на графе, чтобы не запутаться. Концептуальные графы снимают это затруднение, представляя каждое отношение в виде отдельного узла.

Помимо концептуальных графов существуют и другие модификации семантических сетей, это является ещё одной основой для классификации (по реализации).

 

Виды отношений в семантической сети.

Наиболее часто возникает потребность в описании отношений между элементами, множествами и частями объектов. Отношение между объектом и множеством, обозначающим, что объект принадлежит этому множеству, называется отношением классификации (ISA). Говорят, что множество (класс) классифицирует свои экземпляры. Название произошло от английского "IS A" ("суть"). Иногда это отношение именуют также MemberOf. Связь ISA предполагает, что свойства объекта наследуются от множества. Обратное к ISA отношение используется для обозначения примеров, поэтому так и называется – "Example" ("например").

Отношение между надмножеством и подмножеством называется AKO – "A Kind Of" ("разновидность"). Элемент подмножества называется гипонимом, а надмножества – гиперонимом, а само отношение называется отношением гипонимии. Альтернативные названия – "SubsetOf" ("подмножество"). Это отношение определяет, что каждый элемент первого множества входит и во второе,  что первое не больше второго, и свойства первого множества наследуются вторым.

Важным отношением является HasPart, описывающее части/целые объекты (отношение меронимии). Мероним – это объект, являющийся частью для другого. Холоним – это объект, который включает в себя другое.

Также в конкретных системах могут быть рассмотрены следующие типы отношений (наиболее употребительные):

1.                Функциональные (глагольные) – "производить", "влиять" …;

2.                Количественные – "больше", "меньше" …;

3.                Пространственные – "далеко", "за", "под", …;

4.                Временные – "раньше", "в течение", …;

5.                Логические – "И", "Или", …;

6.                Лингвистические – значения слов в конкретной области.

В этом случае сеть строится на основе класса (понятия), а вершины, дуги (отношения) и процедуры представлены как объекты. Процедуры делятся на два подтипа

1.                Действия над дугами – связями:

-   установление связи,

-   аннулирование связи,

-   подсчёт числа вершин, соединённых заданной дугой,

-   проверка наличия/отсутствия связи между заданными вершинами.

2.                Действия над вершинами:

-   определение экземпляра связи;

-   аннулирование экземпляра,

-   подсчёт числа экземпляров, принадлежащих классу,

-   проверка принадлежности экземпляра к некоторому классу.

Благодаря такому подходу с помощью семантических сетей можно представить процедурные знания.

 

2. Онтологии

Онтология (в информатике) – это попытка всеобъемлющей и детальной формализации некоторой области знаний с помощью концептуальной схемы. Обычно такая схема состоит из иерархической структуры данных, содержащей все классы объектов, их связи и правила (теоремы, ограничения), принятые в этой области.

Современные онтологии строятся по большей части одинаково, независимо от языка написания. Обычно они состоят из экземпляров, понятий, атрибутов и отношений.

Экземпляры (instances) (или индивиды – individuals) – это основные, нижнеуровневые компоненты онтологии. Экземпляры могут представлять собой как физические объекты (люди, дома, планеты), так и абстрактные (числа, слова). Однако одной из главных целей онтологии является классификация таких объектов.

Понятия (concepts) (или классы – classes) – это абстрактные группы, коллекции или наборы объектов. Они могут включать в себя экземпляры, другие классы, либо же сочетания и того, и другого.

Объекты в онтологии могут иметь атрибуты. Каждый атрибут имеет, по крайней мере, имя и значение, и используется для хранения информации, которая специфична для объекта и привязана к нему. Значение атрибута может быть сложным типом данных.

Важная роль атрибутов заключается в том, чтобы определять зависимости (отношения) между объектами онтологии. Обычно отношением является атрибут, значением которого является другой объект.

Онтологии делятся на специализированные и общие. Специализированные (предметно-ориентированные) онтологии – это представление какой-либо области знаний или части реального мира. В такой онтологии содержатся специальные для этой области значения терминов. Общие онтологии используются для представления понятий, общих для большого числа областей. Такие онтологии содержат базовый набор терминов, глоссарий или тезаурус, используемый для описания терминов предметных областей.

Если использующая специализированные онтологии система развивается, то может потребоваться их объединение. Это серьёзная задача, так как онтологии часто несовместимы друг с другом, хотя могут представлять близкие области. Разница может появляться из-за особенностей местной культуры, идеологии и т. п., или вследствие использования другого языка описания.

Сегодня объединение онтологий приходится выполнять вручную, это трудоёмкий, медленный и дорогостоящий процесс. Использование базисной онтологии – единого глоссария – несколько упрощает эту работу.

Языки описания онтологий – формальные языки, используемые для кодирования онтологии. Перечислим наиболее распространённые:

1.                OWL – ontology web language, стандарт W3C, язык для семантических утверждений, разработанный как расширение RDF и RDFS;

2.                KIF (Knowledge Interchange Format или формат обмена знаниями) – основанный на S-выражениях синтаксис для логики;

3.                CycL – онтологический язык, использующийся в проекте Cyc, основан на исчислении предикатов с некоторыми расширениями более высокого порядка.

Для работы с языками онтологий существует несколько видов технологий: редакторы онтологий (для создания онтологий), DBMS онтологий (для хранения и обращения к онтологии) и хранилища онтологий (для работы с несколькими онтологиями).

 

3. Семантическая паутина.

Семантическая паутина – это надстройка над существующей Всемирной паутиной, которая призвана сделать размещённую в ней информацию более понятной для компьютеров. Данная концепция предложена и продвигается Консорциумом W3 (её основателем сэром Тимом Бернерсом-Ли в мае 2001 года). Она основывается на двух идеях:

1.                Повсеместное использование универсальных идентификаторов ресурсов (URI). Традиционная схема использования таких идентификаторов в современном Интернете сводится к установке ссылок, ведущих на объект, основным свойством которого является возможность его "закачки" (веб-страница, файл произвольного содержания, физическому ресурсу по протоколу и т.п.). Концепция семантической паутины расширяет это понятие, включая в него ресурсы, недоступные для скачивания. Адресуемыми с помощью URI ресурсами могут быть, например, отдельные люди, города и другие географические сущности, художественные артефакты и т. д. К идентификатору предъявляются несколько простых требований: он должен быть строкой определённого формата, уникальной, а также адресующей реально существующий объект.

2.                Повсеместное использование онтологий и языков описания метаданных. Современные методы автоматической обработки данных, доступных в Интернете, в большинстве своём, основаны на частотном и лексическом анализе текстового содержимого, которое прежде всего предназначено для восприятия человеком. В семантической паутине предлагается использовать форматы описания, доступные для машинной обработки, в свою очередь, использующие URI для адресации описываемых и описывающих объектов, а также онтологии и дескриптивные логики в качестве базовых математических инструментов.

Проблемами реализации семантической паутины являются:

1.                Человеческий фактор – дополнительные трудозатраты и психологические сложности перехода на форматы метаданных;

2.                Философский фактор – утверждение о том, что не возможно построить метаданные самого верхнего уровня, что критично для концепции семантической паутины;

3.                Дублирование информации – она должна быть представлена двумя способами: удобным для восприятия человеком и для восприятия компьютером.

Сейчас ведутся активные разработки по внедрению и апробации новых форматов. Одним из известных проектов является "Дублинское ядро" – он направлен на внедрение новых форматов метаданных и разработке на них создающихся Web-приложений.

 

 

Тема 7. Работа со знаниями, представленными в текстовом виде.

План:

1.                Гипертекстовые информационные технологии.

2.                Информационно-поисковые системы.

3.                Системы распознавания образов.

 

1. Гипертекстовые информационные технологии

Текст является универсальным средством представления, накопления и передачи знаний в человеческом обществе. Поэтому технологии работы с естественно-языковыми текстами (а также с текстами на ограниченном естественном языке) всегда считались важнейшими для ИИ.

Гипертекст – одна из фундаментальных моделей представления знаний, выраженных в текстовом виде. Под гипертекстом понимается форма организации семантической информации, предусматривающая ее разделение на фрагменты, для каждого из которых заданы переходы к родственным фрагментам. В настоящее время под ГТ также понимают многоцелевой информационный фонд, характеризующийся полнотой изложения сведений по определенной тематике и наличием ссылок между статьями.

В основе ГТ лежат следующие основные идеи.

1.                Текст разбивается на фрагменты, представляющие его семантические единицы (сеты). Между ними устанавливаются связи, которые могут наделяться именами.

2.                В отличие от обычного текста, который читается последовательно (в порядке, определенном его автором), ГТ можно читать, двигаясь по разным траекториям, образованным связанными сетами.

3.                Активируемые переходы выбираются читателем (пользователем). Имена (типы) связей облегчают решение задачи выбора перехода.

Гипертекстовая информационная технология – технология обработки семантической информации, основанная на использовании ГТ. Она относится к проблематике ИИ, так как ее содержанием является представление, поиск и обработка семантической информации, выраженной в текстах. Области применения гипертекстовой технологии весьма разнообразны: информационные ресурсы и технологии Internet; гипертекстовые информационно-поисковые системы; гипертекстовые информационные модели экономических систем; базы данных с гипертекстовой организацией; обучающие системы; экспертные системы и т.д.

В соответствии с протоколом передачи гипертекста HyperText Transport Protocol (HTTP) минимальной неделимой единицей данных, предназначенной для межмашинного обмена, является текст, записанный на языке разметки гипертекста HyperText Markup Language (HTML). Логически единая система HTML-страниц может быть физически рассредоточена по сети. Система URL позволяет как размещать, так и собирать ресурсы, на которые ссылается ГТ.

В основе моделей ГТ лежит понятие информационно-справочной статьи, выступающей в качестве информационной единицы ГТ. В конкретных технологиях они называются по-разному: страница, статья, тема и др. Статья может включать информацию, представленную в разных формах: текст, таблицы, фрагменты программного кода (макросы, скрипты), внедренные цифровые объекты, а также ссылки на подобные объекты (графика, звук, видео, управляющие элементы пользовательского интерфейса и т. д.), включаемые в ИСС при ее загрузке.

 

2. Информационно-поисковые системы

Гипертекстовая информационная технология используется при организации больших массивов текстовых документов и реализации методов поиска информации в них.

Информационный поиск – совокупность операций, методов и процедур, направленных на отбор данных, хранящихся в ИС и соответствующих заданным условиям.

Информационно-поисковые системы (ИПС) подразделяются на три класса: документальные; фактографические; гипертекстовые.

Документальные ИПС хранят и выдают сведения о документах, основное содержимое которых представлено в виде связанного текста на ЕЯ. Признаки документа, отражающие его содержание в ИПС, называют поисковым образом, а признаки запроса к ИПС — поисковым предписанием. Процедура перевода документа и запроса в форму представления, принятую в ИПС, называется индексированием.

В фактографических ИПС хранятся не документы, а собственно сведения (факты) об объектах ПрО. Подобные ИПС реализуются, в частности, на основе реляционных БД.

В гипертекстовых ИПС кроме содержимого документов отражается их семантическая структура. Поэтому по глубине формализации ГИПС занимают промежуточное положение между документальными и фактографическими ИПС.

Гипертекст расширяет возможности человека, связанные с поиском и обработкой информации, за счет установления ассоциаций, построения обобщений, формирования целостного представления о содержании документа и т. д. В настоящее время существует тенденция интеграции гипертекстовых ИС со специализированными пакетами прикладных программ. При этом возникают гибридные ИС, предназначенные для решения различных классов трудноформализуемых задач.

В информационно-поисковых системах применяются следующие методы поиска:

1.                индексирование текстов и поиск по ключевым словам (по индексу);

2.                поиск, включающий морфологический разбор и отождествление различных грамматических форм слов;

3.                поиск с ранжированием документов по степени релевантности запросу;

4.                использование формальных поисковых языков;

5.                комплексные методы.

В технологиях БД и БЗ наряду с перечисленными применяются следующие методы поиска:

1.                использование формальных языков запросов, позволяющих описывать условия совместного вхождения ключевых слов в документ (это направление представляют SQL-подобные языки);

2.                методы семантического анализа текста.

Средства автоматического извлечения знаний из текстовых ресурсов Internet реализуются в поисковых машинах. При этом различают:

1.                методы итеративного поиска;

2.                методы поиска по выборке;

3.                методы, использующие каталоги (рубрикаторы и классификаторы, организующие множество документов в деревья);

4.                семантические методы поиска, использующие подходы ИИ.

Количество информации, представленной в Internet, постоянно увеличивается, поэтому необходимы автоматизированные средства поиска. Наиболее распространёнными являются каталоги ресурсов и поисковые машины. Каталог ресурсов Internet – постоянно обновляемая и пополняемая система ссылок на ресурсы, распределенные по иерархической структуре категорий. Поисковые машины (или поисковые системы) позволяют находить ресурсы Internet непосредственно по их текстовому содержимому. Функционирование поисковой машины включает два базовых процесса: 1) индексирование ресурсов Internet (автоматическое построение и обновление индекса); 2) поиск по индексу по запросам пользователей.

 

3. Системы распознавания образов

Методы автоматического распознавания образов и их реализация в системах оптического чтения текстов (OCR-системах – Optical Character Recognition) – одна из самых востребованных технологий искусственного интеллекта. OCR понимается как автоматическое распознавание с помощью специальных программ изображений символов печатного или рукописного текста (например, введенного в компьютер с помощью сканера) и преобразование его в формат, пригодный для обработки текстовыми процессорами. Автоматическое чтение печатных и рукописных текстов является частным случаем автоматического визуального восприятия сложных изображений. Многочисленные исследования показали, что для полного решения этой задачи необходимо интеллектуальное распознавание, т. е. «распознавание с пониманием». Однако, в настоящее время в технически реализуемых OCR-системах рассматриваемая проблема значительно упрощена и сведена к задаче классификации по признакам простых объектов. Эта задача описывается хорошо разработанным математическим аппаратом пороговых отделителей – разделяющими плоскостями

В лучших OCR-системах используется технология распознавания, свойственная человеку, реализуемая по схеме: обработка контекста – грубое выделение признаков – выдвижение гипотезы об объекте   выделение составных частей – проверка правильности – переход от гипотезы к утверждению (перевод предположения в заключение).

Выделяются три принципа, на которых основаны все OCR-системы

1.                Принцип целостности образа: в исследуемом объекте всегда есть значимые части, между которыми существуют отношения.

2.                Принцип целенаправленности: распознавание является целенаправленным процессом выдвижения и проверки гипотез (поиска того, что ожидается от объекта).

3.                Принцип адаптивности: распознающая система должна быть способна к самообучению.

Графический образ символа на выходе сканера имеет вид шейпа, представляющего собой матрицу из точек, которую можно редактировать поэлементно. Реализация соответствующих механизмов связана с решением проблемы понимания текста на естественном языке. Общее решение задачи автоматического распознавания образов должно основываться на организации процесса с такими интеллектуальными составляющими, как целостность восприятия, целенаправленность, предвидение (выдвижение гипотез), максимальное использование контекста и знаний о среде (в пределе — использование модели мира), т. е. учете и реализации интеллектуальных механизмов зрительного восприятия человека.

Автоматическое зрительное восприятие на сегодняшний день не достигает совершенства человеческого восприятия образов. Главная причина этого заключается в неумении строить достаточно полные и семантически выразительные компьютерные модели ПрО.

Среди OCR-технологий важное значение имеют специальные технологии решения отдельных классов задач автоматического распознавания образов:

1.                поиск людей по фотографиям;

2.                поиск месторождений полезных ископаемых и прогнозирование погоды по данным аэрофотосъемки и снимкам со спутников в различных диапазонах светового излучения;

3.                составление географических карт по исходной информации, используемой в предыдущей задаче;

4.                анализ отпечатков пальцев и рисунков радужной оболочки глаза в криминалистике, охранных и медицинских системах.

Для решения этих задач созданы специальные методы и алгоритмы, объединенные в специализированные интеллектуальные информационные системы.

 

 

Тема 8. Понятие экспертной системы.

План:

1.                Определение экспертной системы.

2.                Общая архитектура экспертных систем.

3.                Классификация экспертных систем.

4.                Характеристики экспертных систем

 

 

1. Определение экспертной системы.

В начале 80-х годов в исследованиях по искусственному интеллекту сформировалось самостоятельное направление, получившее название "экспертные системы" (ЭС), которое также называют "инженерия знаний".

Экспертная система – это программное средство, использующее знания и опыт экспертов для обеспечения высокоэффективного решения неформализованных задач в узкой предметной области. Основу ЭС составляет база знаний о предметной области, которая содержит факты (статические сведения о предметной области) и правила - набор инструкций, применяя которые к известным фактам можно получать новые факты.

Экспертные системы отличаются от систем обработки данных тем, что в них используется символьный (а не числовой) способ представления и вывода знаний, а также эвристический поиск решения вместо следования жестко заданному алгоритму. ЭС используются для решения так называемых неформализованных задач, общим для которых является то, что

1.                ошибочностью, неоднозначностью, неполнотой и противоречивостью исходных данных;

2.                ошибочностью, неоднозначностью, неполнотой и противоречивостью знаний о проблемной области и решаемой задаче;

3.                большой размерностью пространства решения, т.е. перебор при поиске решения весьма велик;

4.                динамически изменяющимися данными и знаниями;

5.                не существует алгоритмического решения задачи, а если алгоритмическое решение есть, то его нельзя использовать из-за ограниченности ресурсов (время, память).

Использование систем искусственного интеллекта и, в частности, экспертных систем, имеет ряд преимуществ:

1.                технология экспертных систем существенно расширяет круг задач (в частности, социально-экономических), решаемых на компьютерах;

2.                объединение технологии ЭС с технологией традиционного программирования позволяет конечным пользователям, а не специалистам-программистам, динамично модифицировать приложения под текущие задачи;

3.                так как в большинстве ЭС знания хранятся на ограниченном естественном языке, то упрощается обучение персонала и сопровождение системы;

4.                применение для решения проблем высококачественного опыта, который представляет уровень мышления наиболее квалифицированных экспертов в данной области, что ведёт к решениям творческим, точным и эффективным;

5.                наличие прогностических возможностей, при которых ЭС выдаёт ответы не только для конкретной ситуации, но и показывает, как изменяются эти ответы в новых ситуациях, с возможностью подробного объяснения каким образом новая ситуация привела к изменениям;

6.                обеспечение институциональной памяти, за счёт входящей в состав ЭС базы знаний, которая разработана в ходе взаимодействий со специалистами организации, и представляет собой текущую политику этой группы людей, что позволяет накапливать и концентрировать опыт лучших сотрудников организации;

7.                постоянство системы с точки зрения профессиональной подготовки, т.к. человеческая компетенция ослабевает со временем;

8.                лёгкость передачи или воспроизведения накопленных знаний, т.к. это простой процесс копирования программы или файла данных;

9.                минимизация влияния человеческого фактора (эмоционального и психолого-физического состояния эксперта);

10.           разработка большинства ЭС дорога, но они дёшевы в эксплуатации, тогда как и подготовка, и использование экспертов очень дороги.

 

2. Общая архитектура экспертных систем.

Архитектура экспертных систем включает в себя 2 основных компонента:

-                    базу знаний (хранилище единиц знаний) – совокупность единиц знаний, которые представляют собой формализованное с помощью некоторого метода представления знаний отражение объектов предметной области и их взаимосвязь, действия над объектами и, возможно, неопределённости, с которыми эти действия осуществляются;

-                    программный инструмент доступа и обработки знаний, состоящий из механизма логического вывода заключений и механизма приобретения знаний.

Главной особенностью, характеризующей экспертные системы как отдельный класс информационных систем, является модуль объяснения полученного результата. Его работа строится на основе механизма логического вывода решения, с фиксированием каким образом на каждом из шагов был получен результат.

 

Интеллектуальный интерфейс ориентирован на организацию дружественного общения с пользователем, как в ходе решения задач, так и в процессе объяснения результатов работы. Основная задача интеллектуального интерфейса – преобразовывать запросы пользователя с естественного языка во внутреннее представление этих запросов.

Механизм логического вывода – программный инструмент, который получает от интеллектуального интерфейса преобразованные запросы, формирует из базы знаний конкретный алгоритм решения задачи, выполняет его, а полученный результат предоставляется интеллектуальному интерфейсу для выдачи ответа на запрос пользователя. В основе использования механизма логического вывода лежит процесс нахождения в соответствии с поставленной целью и описанием конкретной ситуации (исходных данных) относящихся к решению единиц знаний (правил, объектов, прецедентов и т.д.) и связыванию их при необходимости в цепочку рассуждений, приводящую к определённому результату. Для представления знаний в форме правил используется одна из моделей представления знаний.

Механизм объяснения – в процессе или по результатам решения задач пользователь может запросить объяснение или обоснование хода решения. С этой целью экспертная система должна предоставить механизм объяснения.

Рабочая область – часть экспертной системы, связанная с механизмом вывода и предназначенная для хранения вновь полученных правил или фактов. Также в рабочей области происходит непосредственно обработка цепочки вывода решений.

Механизм приобретения знаний – это база знаний, которая отражает знания экспертов, специалистов в данной проблемной области, о действиях в различных ситуациях или процессах решения характерных (типовых) задач. Выявлением подобных знаний и последующим их представлением в базе знаний занимаются специалисты, называемые инженерами знаний. В простейшем случае в качестве механизма приобретения знаний может выступать интеллектуальный редактор, который позволяет вводить единицы знаний в базу знаний и проводить их синтаксический и семантический контроль (к примеру, на непротиворечивость). В более сложных случаях можно извлекать знания путём специальных сценариев интервьюирования экспертов, из вводимых примеров реальных ситуаций, из опыта работы самой интеллектуальной системы.

 

3. Классификация экспертных систем.

По степени сложности решаемых задач ЭС можно классифицировать так:

1.           По способу формирования решений ЭС разделяются на 2 класса:

1.1.         Аналитические – выбор решения из множества существующих альтернатив (определение характеристик объекта);

1.2.         Синтетические – генерация неизвестных решений (формирование ответа).

2.           По способу учёта временного признака ЭС могут быть:

2.1.         Статические – решают задачи при неизменяемых в процессе решения данных и знаний. Статические системы осуществляют монотонное, непрерываемое решение задачи от ввода исходных данных до конечного результата;

2.2.         Динамические – допускают изменения в процессе работы, т.е. в системе существует возможность пересмотра и корректировки полученных данных и знаний.

3.           По видам используемых знаний ЭС могут быть:

3.1.         С детерминированными знаниями;

3.2.         С неопределёнными данными (качественная оценка).

4.     По числу используемых источников данных; ЭС могут быть построены с использованием одного или нескольких источников данных.

 

Наиболее употребительной классификацией является функциональная:

1.                Классифицирующие. Являются одним из важнейших подклассов аналитических систем, решают задачи распознавания различных ситуаций (классификации задач) и по набору признаков (факторов) выбирают определённую последовательность действий (алгоритм решения).

2.                Доопределяющие. Применяются при решении задач с неполными или неопределёнными данными. В этом случае ЭС доопределяет знания с помощью эвристических приёмов или обращения к нескольким источникам данных. Типичными представителями доопределяющих систем являются доски объявлений.

3.                Трансформирующие. Используются, когда динамично видоизменяются исходные данные или предметная область в целом. В качестве методов решения задач здесь могут быть: генерация и тестирование гипотез, предположения и использование общих закономерностей предметной области.

4.                Многоагентные. Основной особенностью является интегрирование разнородных источников знаний в рамках одной системы, динамически обменивающихся данными. Для многоагентных систем характерны обработка больших объёмов информации и распределённое решение поставленной задачи.

5.                Самообучающиеся. В их основе лежат методы автоматической классификации реальных ситуаций, примеры которых накапливаются за определённый период и составляют обучающую выборку. Самообучающиеся системы могут быть "с учителем", когда пользователь (инженер по знаниям) непосредственно задаёт, к какому классу принадлежит определяемая ситуаций, и «без учителя», когда по степени близости значений классифицирующих признаков система сама выделяет классы ситуаций

 

4. Характеристики экспертных систем

Экспертная система должна иметь следующие характеристики:

o   Компетентность, ЭС должна демонстрировать такой же уровень профессионализма в конкретной предметной области, что и эксперты-люди. Она должна быть умелой, т.е. применять знания для получения решений эффективно и быстро. Для того, чтобы по-настоящему подражать поведению эксперта-человека, ЭС должна быть робастной. Это подразумевает не только глубокое, но и широкое понимание предмета (т.е. при некорректных или неполных знаниях уметь рассуждать, используя общие знания и методы нахождения решений).

o   Символьные рассуждения. Эксперты решают задачи, используют эвристики с помощью символов и составляющих из них понятий. Знания в основном содержат символьную информацию, соответствующую содержанию некоторого понятия реального мира. Кроме того, ЭС должна уметь манипулировать этими понятиями в произвольной форме, для того чтобы получить самое рациональное решение.

o   Глубина. ЭС должна иметь глубокие знания в узкоспециализированной предметной области, содержащей трудные задачи. Поэтому правила должны быть сложными. Рекомендации, методы представления знаний, организация знаний, необходимых для нахождения решений часто связаны с объёмом и сложностью пространства поиска. Пространство – это много промежуточных и окончательных решений задачи.

o   Самосознание. ЭС имеют знания, позволяющие им рассуждать о своих действиях, и структуру, упрощающую эти рассуждения. Например, если ЭС основана на правилах, то ей легко рассмотреть цепочки выводов, которые она порождает, чтобы прийти к решению задачи. Если заданы ещё и специальные правила, указывающие, что можно сделать с этими цепочками, то можно использовать эти знания для проверки точности, устойчивости решений задачи. Эти знания системы о том, как она рассуждает, называются метазнаниями, т.е. знаниями о знаниях. У большинства ЭС существует так называемый механизм объяснения. Большинство объяснений включает демонстрацию цепочек выводов, объясняющих, на каком основании было применено каждое правило в цепочке. Это нужно для того, чтобы:

-         Пользователи больше доверяли результатам.

-         Ускорить развитие системы.

-         Предположения становились явными, а не подразумеваемыми.

-         Легче предсказать и выявить влияние изменений.

 

ЭС решают следующие задачи:

·        Интерпретация – построение описаний ситуаций по наблюдаемым данным. Применение: распознавание и понимание речи, анализ изображений, определение химической структуры вещества.

·        Прогноз – вывод вероятных следствий из заданной ситуации. Прогноз погоды, дорожно-транспортных происшествий, будущего урожая, военной обстановки.

·        Диагностика – заключение о нарушениях в системе исходя из наблюдений.

·        Проектирование – построение конфигурации системы при ограничениях.

·        Планирование – построение плана действий.

·        Мониторинг – сравнение наблюдений с критическими точками плана.

·        Отладка – выработка рекомендаций по устранению неисправностей.

·        Ремонт – выполнение плана применения выработанной рекомендации.

·        Обучение – диагностика, отладка и исправление поведения ученика.

·        Управление – интерпретация, прогноз, ремонт и мониторинг поведения системы.

 

Примерами решения вышеприведенных задач могут быть:

·        Диагностика – в медицине, электронных схемах, в системах ПО.

·        Проектирование – синтез и компоновка электронных схем, проектировка зданий, составление бюджета и т.д.

·        Планирование – относится к объектам, способным выполнять некоторые функции.

·        Мониторинг – (предупредительные системы) важны для успешного планирования. Выявление опасных мест плана.

·        Система отладки – дает рекомендации относительно ликвидации плохого функционирования системы.

·        Ремонт – системы строят и выполняют планы исправления некоторых обнаруженных дефектов. Встречаются в машинной промышленности, эксплуатации инженерных сетей, авиации, работе компьютеров.

·        Обучение –   АОС – описывают знания обучаемых и планируют акт общения с обучаемыми с целью передачи им необходимых сведений.

·        Управляющие системы – управление воздушным транспортом, ж/д, телефонной связью, боем, деловой активностью. Обеспечивает адаптивное управление всей системой.